/* * * Copyright 2014 http://Bither.net * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * / */ package net.bither.utils; import com.google.common.base.Optional; import com.google.common.base.Strings; import javax.swing.text.DocumentFilter; import javax.swing.text.NumberFormatter; import java.math.BigDecimal; import java.text.*; import java.util.Locale; /** * <p>Utilities to provide the following to application:</p> * <ul> * <li>Parsing numeric values</li> * </ul> * * @since 0.0.1 */ public class Numbers { /** * Utilities have private constructor */ private Numbers() { } /** * <p>Locale aware method of validating numbers - uses the currentConfiguration locale </p> * * @param text The text representing a number (null or empty is not a number) * @return True if the text can be converted into a number */ public static boolean isNumeric(String text) { return isNumeric(text, LocaliserUtils.getLocale()); } /** * <p>Locale aware method of validating numbers</p> * * @param text The text representing a number (null or empty is not a number) * @param locale Locale to use for number conversion * @return True if the text can be converted into a number */ public static boolean isNumeric(String text, Locale locale) { if (!Strings.isNullOrEmpty(text)) { // Ensure spaces are converted to non-breaking spaces (ASCII 160) text = text.replace(" ", "\u00a0"); // The current configuration will provide the locale NumberFormat formatter = NumberFormat.getInstance(locale); // Keep track of the parse position ParsePosition pos = new ParsePosition(0); formatter.parse(text, pos); // If position is unchanged then no number was found return text.length() == pos.getIndex(); } else { return false; } } /** * <p>Configuration aware method of parsing numbers including separators</p> * * @param text The text representing a number (null or empty is not a number, can include grouping symbols) * @return A BigDecimal representing the parsed number using the String */ public static Optional<BigDecimal> parseBigDecimal(String text) { if (!Strings.isNullOrEmpty(text)) { // Ensure spaces are converted to non-breaking spaces (ASCII 160) text = text.replace(" ", "\u00a0"); DecimalFormat format = newDecimalFormat("#,##0.000000000000000", 8); // Keep track of the parse position ParsePosition pos = new ParsePosition(0); Number value = format.parse(text, pos); // If position is unchanged then no number was found if (text.length() != pos.getIndex()) { return Optional.absent(); } return Optional.of(new BigDecimal(value.toString())); } else { return Optional.absent(); } } /** * @param decimalPlaces The number of decimal places to allow * @param maxLength The overall maximum length * @return A number formatter for editing numbers based on the current configuration */ public static NumberFormatter newEditFormatter(int decimalPlaces, int maxLength) { return newNumberFormatter("#,###.###############", decimalPlaces, maxLength); } /** * @param decimalPlaces The number of decimal places to allow * @param maxLength The overall maximum length * @return A number formatter for displaying numbers based on the current configuration */ public static NumberFormatter newDisplayFormatter(int decimalPlaces, int maxLength) { return newNumberFormatter("#,##0.000000000000000", decimalPlaces, maxLength); } /** * @param pattern The pattern to use (e.g. "#,##0.000000000000000") * @param decimalPlaces The number of decimal places to allow * @param maxLength The overall maximum length * @return A number formatter configured for insert mode and enforced maximum length length */ private static NumberFormatter newNumberFormatter(String pattern, int decimalPlaces, int maxLength) { DecimalFormat decimalFormat = newDecimalFormat(pattern, decimalPlaces); NumberFormatter numberFormatter = newNumberFormatter(decimalFormat, maxLength); // Ensure we keep insert mode numberFormatter.setOverwriteMode(false); return numberFormatter; } /** * @param pattern The pattern to use * @param decimalPlaces The number of decimal places to allow * @return A decimal format based on the current configuration */ private static DecimalFormat newDecimalFormat(String pattern, int decimalPlaces) { // Adjust edit/display formats to the current configuration String groupingSeparator = ""; String decimalSeparator = ""; // Use locale decimal formatting then override with current configuration DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator(decimalSeparator.charAt(0)); symbols.setGroupingSeparator(groupingSeparator.charAt(0)); symbols.setMonetaryDecimalSeparator(decimalSeparator.charAt(0)); // Identify the location of the decimal in the template before locale adjustments int decimalIndex = pattern.indexOf('.'); // Adjust patterns to accommodate the required decimal places if (decimalPlaces > 0) { pattern = pattern.substring(0, decimalIndex + decimalPlaces + 1); } else { pattern = pattern.substring(0, decimalIndex); } // Create a decimal format using the configured symbols for edit/display return new DecimalFormat(pattern, symbols); } /** * @param decimalFormat The decimal format appropriate for this locale * @return A number formatter that is locale-aware and configured for doubles */ private static NumberFormatter newNumberFormatter(final DecimalFormat decimalFormat, final int maxEditLength) { // Create the number formatter with local-sensitive adjustments NumberFormatter displayFormatter = new NumberFormatter(decimalFormat) { // The max input length for the given symbol DocumentFilter documentFilter = new DocumentMaxLengthFilter(maxEditLength); @Override public Object stringToValue(String text) throws ParseException { // RU locale (and others) requires a non-breaking space for a grouping separator text = text.replace(' ', '\u00a0'); return super.stringToValue(text); } @Override protected DocumentFilter getDocumentFilter() { return documentFilter; } }; // Use a BigDecimal for widest value handling displayFormatter.setValueClass(BigDecimal.class); return displayFormatter; } }